package org.ktpro.shortlink.admin.common.idempotent;

import cn.hutool.crypto.digest.DigestUtil;
import com.alibaba.fastjson2.JSON;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.ktpro.shortlink.admin.common.biz.user.UserContext;
import org.ktpro.shortlink.admin.common.convention.exception.ClientException;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;

/**
 * 防止重复提交表单的切面控制器
 */

@Aspect
@RequiredArgsConstructor
@Component
public final class NoRepeatSubmitAspect {
    private final RedissonClient redissonClient;

    /**
     * 增强方法标记 {@link NoRepeatSubmit} 注解逻辑
     * 其实就是对注解拦截到的方法进行加强，如果出现重复提交就进行提示并做幂等处理
     */
    @Around("@annotation(org.ktpro.shortlink.admin.common.idempotent.NoRepeatSubmit)")
    public Object noRepeatSubmit(final ProceedingJoinPoint joinPoint) throws Throwable {

        NoRepeatSubmit noRepeatSubmit = getNoRepeatSubmitAnnotation(joinPoint);
        // 获取分布式锁标识
        String lockKey = String.format("no-repeat-submit:path:%s:currentUserId:%s:md5:%s", getServletPath(), getCurrentUserId(), calcArgsMD5(joinPoint));
        RLock lock = redissonClient.getLock(lockKey);
        // 尝试获取锁，获取锁失败就意味着已经重复提交，直接抛出异常
        if (!lock.tryLock()) {
            throw new ClientException(noRepeatSubmit.message());
        }
        Object result;
        try {
            // 执行标记了防重复提交注解的方法原逻辑
            result = joinPoint.proceed();
        } finally {
            lock.unlock();
        }
        return result;
    }

    /**
     * @return 返回自定义防重复提交注解
     */
    public static NoRepeatSubmit getNoRepeatSubmitAnnotation(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method targetMethod = joinPoint.getTarget().getClass().getDeclaredMethod(methodSignature.getName(), methodSignature.getMethod().getParameterTypes());
        return targetMethod.getAnnotation(NoRepeatSubmit.class);
    }


    /**
     * @return 获取当前线程上下文 ServletPath
     */
    private String getServletPath() {
        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        assert sra != null;
        return sra.getRequest().getServletPath();
    }

    /**
     * @return 当前操作用户 ID
     */
    private String getCurrentUserId() {
        // 通过上下文获取用户id
        return UserContext.getUserId();
    }

    // 这里对 joinPoint 加密是为了减少存储redis对象 key 的大小和复杂度，同时也能保证这个唯一标识不会重复
    /**
     * @return joinPoint md5
     */
    private String calcArgsMD5(ProceedingJoinPoint joinPoint) {
        return DigestUtil.md5Hex(JSON.toJSONBytes(joinPoint.getArgs()));
    }
}
